diff options
Diffstat (limited to 'app/[lng]/partners/(partners)')
| -rw-r--r-- | app/[lng]/partners/(partners)/pq_new/[id]/page.tsx | 206 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/pq_new/page.tsx | 298 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/site-visit/page.tsx | 30 |
3 files changed, 534 insertions, 0 deletions
diff --git a/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx b/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx new file mode 100644 index 00000000..5a8313cc --- /dev/null +++ b/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx @@ -0,0 +1,206 @@ +import { Metadata } from "next"; +import Link from "next/link"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft, LogIn } from "lucide-react"; +import { Shell } from "@/components/shell"; +import { getPQById, getPQDataByVendorId } from "@/lib/pq/service"; +import { unstable_noStore as noStore } from 'next/cache'; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { PQInputTabs } from "@/components/pq-input/pq-input-tabs"; + +export const metadata: Metadata = { + title: "사전 평가 (PQ) 작성", + description: "사전 평가 항목을 작성합니다.", +}; + +// 페이지가 기본적으로 동적임을 나타냄 +export const dynamic = "force-dynamic"; + +interface PQEditPageProps { + params: Promise<{ id: string }>; +} + +export default async function PQEditPage(props: PQEditPageProps) { + // 캐시 비활성화 + noStore(); + + const params = await props.params; + const pqSubmissionId = parseInt(params.id, 10); + + // 인증 확인 + const session = await getServerSession(authOptions); + + // 로그인 확인 + if (!session || !session.user) { + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + 사전 평가 (PQ) 작성 + </h2> + </div> + </div> + + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + 사전 평가를 작성하려면 먼저 로그인하세요. + </p> + <Button size="lg" asChild> + <Link href={`/partners?callbackUrl=/partners/pq/${pqSubmissionId}`}> + <LogIn className="mr-2 h-4 w-4" /> + 로그인하기 + </Link> + </Button> + </div> + </div> + </Shell> + ); + } + + // 세션에서 vendorId 가져오기 + const vendorId = session.user.companyId; + + // 벤더 권한 확인 + if (session.user.domain !== "partners" || !vendorId) { + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + 접근 권한 없음 + </h2> + </div> + </div> + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">벤더 계정이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + 벤더 계정으로 로그인해주세요. + </p> + </div> + </div> + </Shell> + ); + } + + const idAsNumber = Number(vendorId); + + try { + // PQ Submission 정보 조회 (vendorPQSubmissions 테이블에서) + const pqSubmission = await getPQById(pqSubmissionId, idAsNumber); + + // 이 PQ가 현재 로그인한 벤더의 것인지 확인 + if (pqSubmission.vendorId !== idAsNumber) { + throw new Error("Access denied - This PQ belongs to another vendor"); + } + + // PQ 데이터 조회 (pqCriterias와 답변) + const pqData = await getPQDataByVendorId(idAsNumber, pqSubmission.projectId || undefined); + + // 상태에 따른 읽기 전용 모드 결정 + const isReadOnly = [ "APPROVED"].includes(pqSubmission.status); + const statusText = pqSubmission.status === "SUBMITTED" ? "제출됨" : + pqSubmission.status === "APPROVED" ? "승인됨" : + pqSubmission.status === "REJECTED" ? "거부됨" : "작성 중"; + + const pageTitle = pqSubmission.type === "PROJECT" + ? `프로젝트 PQ - ${pqSubmission.projectName || pqSubmission.projectCode}` + : pqSubmission.type === "NON_INSPECTION" + ? "미실사 PQ" + : "일반 PQ"; + + // 프로젝트 정보 (프로젝트 PQ인 경우) + const projectPQ = pqSubmission.projectId ? { + id: pqSubmission.projectId, + projectId: pqSubmission.projectId, + projectCode: pqSubmission.projectCode || '', + projectName: pqSubmission.projectName || '', + status: pqSubmission.status, + submittedAt: pqSubmission.submittedAt, + } : null; + + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-4"> + <Button variant="outline" size="sm" asChild> + <Link href="/partners/pq_new"> + <ArrowLeft className="w-4 h-4 mr-2" /> + 목록으로 + </Link> + </Button> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + {pageTitle} + </h2> + <p className="text-muted-foreground"> + 상태: {statusText} + {pqSubmission.status === "REJECTED" && pqSubmission.rejectReason && ( + <span className="text-destructive ml-2"> + (거부 사유: {pqSubmission.rejectReason}) + </span> + )} + </p> + </div> + </div> + </div> + + {/* 읽기 전용 모드 알림 */} + {/* {isReadOnly && ( + <Alert> + <AlertDescription> + 이 PQ는 현재 제출된 상태입니다. SHI 코멘트를 확인 후 재제출이 가능합니다. + </AlertDescription> + </Alert> + )} */} + + {/* PQ 입력 컴포넌트 */} + <PQInputTabs + data={pqData} + vendorId={idAsNumber} + projectId={pqSubmission.projectId || undefined} + projectData={projectPQ} + isReadOnly={isReadOnly} + currentPQ={{ // 현재 PQ Submission 정보 전달 + id: pqSubmission.id, + status: pqSubmission.status, + type: pqSubmission.type + }} + /> + </Shell> + ); + } catch (error) { + console.error("Error loading PQ:", error); + + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + 오류 발생 + </h2> + </div> + </div> + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">PQ를 불러올 수 없습니다</h3> + <p className="mb-6 text-muted-foreground"> + 요청하신 PQ를 찾을 수 없거나 접근 권한이 없습니다. + </p> + <Button asChild> + <Link href="/partners/pq"> + <ArrowLeft className="mr-2 h-4 w-4" /> + 목록으로 돌아가기 + </Link> + </Button> + </div> + </div> + </Shell> + ); + } +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/pq_new/page.tsx b/app/[lng]/partners/(partners)/pq_new/page.tsx new file mode 100644 index 00000000..eea5b21d --- /dev/null +++ b/app/[lng]/partners/(partners)/pq_new/page.tsx @@ -0,0 +1,298 @@ +import * as React from "react"; +import Link from "next/link"; +import { Metadata } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { LogIn, Edit, Eye, Ellipsis } from "lucide-react"; +import { Shell } from "@/components/shell"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { unstable_noStore as noStore } from 'next/cache'; +import { getAllPQsByVendorId, getPQStatusCounts } from "@/lib/pq/service"; +import { InformationButton } from "@/components/information/information-button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; + +export const metadata: Metadata = { + title: "사전 평가 (PQ) 목록", + description: "요청된 사전 평가 목록을 확인하고 작성합니다.", +}; + +// 페이지가 기본적으로 동적임을 나타냄 +export const dynamic = "force-dynamic"; + +function getStatusBadge(status: string) { + switch (status) { + case "REQUESTED": + return <Badge variant="outline">요청됨</Badge>; + case "IN_PROGRESS": + return <Badge variant="secondary">진행 중</Badge>; + case "SUBMITTED": + return <Badge variant="default">제출됨</Badge>; + case "APPROVED": + return <Badge variant="default">승인됨</Badge>; + case "REJECTED": + return <Badge variant="destructive">거부됨</Badge>; + default: + return <Badge variant="outline">{status}</Badge>; + } +} + +function getFormattedDate(date: Date | null) { + if (!date) return "-"; + return new Intl.DateTimeFormat("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }).format(new Date(date)); +} + +export default async function PQListPage() { + // 캐시 비활성화 + noStore(); + + // 인증 확인 + const session = await getServerSession(authOptions); + + // 로그인 확인 + if (!session || !session.user) { + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + 사전 평가 (PQ) 목록 + </h2> + <p className="text-muted-foreground"> + 요청된 사전 평가 목록을 확인하고 작성합니다. + </p> + </div> + </div> + + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + 사전 평가를 확인하려면 먼저 로그인하세요. + </p> + <Button size="lg" asChild> + <Link href="/partners?callbackUrl=/partners/pq"> + <LogIn className="mr-2 h-4 w-4" /> + 로그인하기 + </Link> + </Button> + </div> + </div> + </Shell> + ); + } + + // 세션에서 vendorId 가져오기 + const vendorId = session.user.companyId; + + // 벤더 권한 확인 + if (session.user.domain !== "partners" || !vendorId) { + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + 접근 권한 없음 + </h2> + </div> + </div> + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">벤더 계정이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + 벤더 계정으로 로그인해주세요. + </p> + </div> + </div> + </Shell> + ); + } + + const idAsNumber = Number(vendorId); + + // 데이터 가져오기 (병렬 실행) + const [pqList, pqStatusCounts] = await Promise.all([ + getAllPQsByVendorId(idAsNumber), + getPQStatusCounts(idAsNumber), + ]); + + return ( + <Shell className="gap-6"> + <div className="flex justify-between items-center"> + <div> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight">사전 평가 (PQ) 목록</h2> + <InformationButton pagePath="partners/pq_new" /> + </div> + <p className="text-muted-foreground"> + 요청된 사전 평가 목록을 확인하고 작성합니다. + </p> + </div> + </div> + + {/* PQ 상태 요약 카드 */} + <div className="grid gap-4 md:grid-cols-4"> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">총 PQ</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {Object.values(pqStatusCounts).reduce((sum, count) => sum + count, 0)}건 + </div> + </CardContent> + </Card> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">작성 대기</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {(pqStatusCounts.REQUESTED || 0) + (pqStatusCounts.IN_PROGRESS || 0)}건 + </div> + </CardContent> + </Card> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">제출됨</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {pqStatusCounts.SUBMITTED || 0}건 + </div> + </CardContent> + </Card> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">승인됨</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {pqStatusCounts.APPROVED || 0}건 + </div> + </CardContent> + </Card> + </div> + + {/* PQ 목록 테이블 */} + <Card> + <CardHeader> + <CardTitle>PQ 목록</CardTitle> + </CardHeader> + <CardContent className="p-0"> + <Table> + <TableHeader> + <TableRow> + <TableHead>유형</TableHead> + <TableHead>PQ 번호</TableHead> + <TableHead>프로젝트</TableHead> + <TableHead>상태</TableHead> + <TableHead>요청일</TableHead> + <TableHead>제출일</TableHead> + <TableHead>승인일</TableHead> + <TableHead>액션</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {pqList.length === 0 ? ( + <TableRow> + <TableCell colSpan={8} className="text-center py-8 text-muted-foreground"> + 요청된 PQ가 없습니다. + </TableCell> + </TableRow> + ) : ( + pqList.map((pq) => { + const canEdit = ["REQUESTED", "IN_PROGRESS", "REJECTED"].includes(pq.status); + const canView = ["SUBMITTED", "APPROVED"].includes(pq.status); + + return ( + <TableRow key={pq.id}> + <TableCell> + <Badge variant={ + pq.type === "PROJECT" ? "default" : + pq.type === "NON_INSPECTION" ? "secondary" : + "outline" + }> + {pq.type === "PROJECT" ? "프로젝트" : + pq.type === "NON_INSPECTION" ? "미실사" : + "일반"} + </Badge> + </TableCell> + <TableCell> + {pq.pqNumber || "-"} + </TableCell> + <TableCell> + {pq.projectName || "-"} + </TableCell> + <TableCell> + {getStatusBadge(pq.status)} + </TableCell> + <TableCell> + {getFormattedDate(pq.createdAt)} + </TableCell> + <TableCell> + {getFormattedDate(pq.submittedAt)} + </TableCell> + <TableCell> + {getFormattedDate(pq.approvedAt)} + </TableCell> + <TableCell> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="액션 메뉴 열기" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-36"> + {canEdit && ( + <DropdownMenuItem asChild> + <Link href={`/partners/pq_new/${pq.id}`}> + <Edit className="mr-2 h-4 w-4" /> + 작성 + </Link> + </DropdownMenuItem> + )} + {canView && ( + <DropdownMenuItem asChild> + <Link href={`/partners/pq_new/${pq.id}`}> + <Eye className="mr-2 h-4 w-4" /> + 보기 + </Link> + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + </TableCell> + </TableRow> + ); + }) + )} + </TableBody> + </Table> + </CardContent> + </Card> + </Shell> + ); +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/site-visit/page.tsx b/app/[lng]/partners/(partners)/site-visit/page.tsx new file mode 100644 index 00000000..92580b35 --- /dev/null +++ b/app/[lng]/partners/(partners)/site-visit/page.tsx @@ -0,0 +1,30 @@ +import { getServerSession } from "next-auth"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import { getSiteVisitRequestsByVendorId } from "@/lib/site-visit/service"
+import { ClientSiteVisitWrapper } from "@/lib/site-visit/client-site-visit-wrapper"
+import { unstable_noStore as noStore } from 'next/cache'
+
+// 페이지가 기본적으로 동적임을 나타냄
+export const dynamic = "force-dynamic"
+
+export default async function SiteVisitPage() {
+ // Opt out of caching for this route
+ noStore()
+
+ // 세션
+ const session = await getServerSession(authOptions)
+ // 세션에서 vendorId 가져오기
+ const vendorId = session?.user.companyId
+ const idAsNumber = Number(vendorId)
+
+ // 방문실사 요청 목록 가져오기
+ const siteVisitRequests = await getSiteVisitRequestsByVendorId(idAsNumber)
+
+ // 클라이언트 컴포넌트로 데이터 전달
+ return (
+ <ClientSiteVisitWrapper
+ siteVisitRequests={siteVisitRequests}
+ vendorId={idAsNumber}
+ />
+ )
+}
\ No newline at end of file |
